﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Windows.Forms;
using System.ComponentModel;
using System.Reflection;
using System.Linq.Expressions;

namespace JV.MVVM {

    class BindHold {

        Control control;
        INotifyPropertyChanged context;

        public BindHold( Control control, ParseNode tree, INotifyPropertyChanged context ) {

            this.control = control;
            this.context = context;
            ContextGets = new Dictionary<string, GetHandler>( );

            var ctrProperty = tree.Nodes[ 0 ].Token.Text;
            var Way = tree.Nodes[ 1 ];
            var Exp = tree.Nodes[ 2 ];
            var prop = control.GetType( ).GetProperty( ctrProperty );

            if ( Exp.Token.Type == TokenType.AddExpr ) {
                // this is a property or event
                if ( prop != null ) {
                    // property
                    BindProperty( prop, Way, Exp );
                }
                else {
                    // event
                    BindEvent( ctrProperty, Exp );
                }

            }
            else {
                // this is a collection
                BindCollection( prop, Way, Exp );
            }

        }

        #region property binding

        SetHandler ControlPropertySet;
        GetHandler ControlPropertyGet;
        Func<object, object> CompiledExpression;

        Dictionary<string, GetHandler> ContextGets;
        SetHandler ContextSet;

        void BindProperty( PropertyInfo ControlProperty, ParseNode Way, ParseNode Exp ) {

            var propName = GetSimplePropertyName( Exp );

            // if this is an expression, and the binding way is two way, then there is no way to push back the typed value into an expression, so just return
            if ( ( propName == null ) && ( Way.Token.Type == TokenType.TWOWAY ) ) return;

            ControlPropertySet = Emmiter.CreateSetHandler( ControlProperty );
            CompiledExpression = Compile( Exp );

            SetControlValue( );

            if ( Way.Token.Type == TokenType.ONEWAY ) {
                context.PropertyChanged += new PropertyChangedEventHandler( context_PropertyChanged );
            }

            if ( Way.Token.Type == TokenType.TWOWAY ) {
                var evName = ControlProperty.Name + "Changed";
                var contextProp = context.GetType( ).GetProperty( propName );
                if ( contextProp != null ) {
                    ContextSet = Emmiter.CreateSetHandler( contextProp );
                    ControlPropertyGet = Emmiter.CreateGetHandler( ControlProperty );
                    AttachToEvent( evName, ( ) => ContextSet( context, ControlPropertyGet( control ) ) );
                }
            }

        }

        void context_PropertyChanged( object sender, PropertyChangedEventArgs e ) {
            if ( ContextGets.ContainsKey( e.PropertyName ) ) SetControlValue( );
        }

        void SetControlValue( ) {
            ControlPropertySet( control, CompiledExpression( context ) );
        }

        Func<object, object> Compile( ParseNode Exp ) {

            ParameterExpression value = Expression.Parameter( typeof( object ), "value" );
            var compiled = Visit( Exp, value );

            if ( compiled != null ) {
                var exp = Expression.ConvertChecked( compiled, typeof( object ) );
                var lambda = Expression.Lambda( exp, new ParameterExpression[ ] { value } );
                var del = lambda.Compile( );
                return ( Func<object, object> )del;
            }

            return null;

        }

        Expression Visit( ParseNode Exp, ParameterExpression value ) {

            try {
                Expression Ret = null;

                switch ( Exp.Token.Type ) {
                    case TokenType.AddExpr:
                        for ( int i = 0; i < Exp.Nodes.Count; i++ ) {
                            var currentNode = Exp.Nodes[ i ];
                            if ( i == 0 ) {
                                Ret = Visit( currentNode, value );
                            }
                            else {
                                i++;
                                if ( i == Exp.Nodes.Count ) return Ret;
                                switch ( currentNode.Token.Text ) {
                                    case "+": Ret = Expression.AddChecked( Ret, Visit( Exp.Nodes[ i ], value ) ); break;
                                    case "-": Ret = Expression.SubtractChecked( Ret, Visit( Exp.Nodes[ i ], value ) ); break;
                                    case "&":
                                        Expression<Func<object, object, string>> concat = ( s1, s2 ) => string.Concat( s1, s2 );
                                        Ret = Expression.Invoke( concat, Expression.Convert( Ret, typeof( object ) ), Visit( Exp.Nodes[ i ], value ) );
                                        break;
                                    default: break;
                                }
                            }
                        }
                        return Ret;

                    case TokenType.MultExpr:
                        for ( int i = 0; i < Exp.Nodes.Count; i++ ) {
                            var currentNode = Exp.Nodes[ i ];
                            if ( i == 0 ) {
                                Ret = Visit( currentNode, value );
                            }
                            else {
                                i++;
                                if ( i == Exp.Nodes.Count ) return Ret;
                                switch ( currentNode.Token.Text ) {
                                    case "*": Ret = Expression.MultiplyChecked( Ret, Visit( Exp.Nodes[ i ], value ) ); break;
                                    case "/": Ret = Expression.Divide( Ret, Visit( Exp.Nodes[ i ], value ) ); break;
                                    default: break;
                                }
                            }
                        }
                        return Ret;

                    case TokenType.DottedIdent:
                        var propName = Exp.Nodes[ 0 ].Token.Text;

                        Expression<Func<ParseNode, object>> GetMyValue = tn => GetContextValue( tn );
                        return Expression.Invoke( GetMyValue, Expression.Constant( Exp ) );

                    case TokenType.Atom:
                        if ( Exp.Nodes.Count == 1 ) return Visit( Exp.Nodes[ 0 ], value );
                        else return Visit( Exp.Nodes[ 1 ], value );

                    case TokenType.NUMBER:
                        return Expression.Constant( decimal.Parse( Exp.Token.Text ) );

                    case TokenType.STRING:
                        var str = Exp.Token.Text.Substring( 1 );
                        str = str.Substring( 0, str.Length - 1 );
                        return Expression.Constant( str );

                    default:
                        return Visit( Exp.Nodes[ 0 ], value );

                }

            }
            catch ( Exception e ) {
                throw e;
            }

        }

        object GetContextValue( ParseNode Exp ) {

            if ( Exp.Nodes.Count == 1 ) {
                // just a property
                var propName = Exp.Nodes[ 0 ].Token.Text;
                if ( !ContextGets.ContainsKey( propName ) ) {
                    var propInfo = context.GetType( ).GetProperty( propName );
                    if ( propInfo != null ) {
                        ContextGets.Add( propName, Emmiter.CreateGetHandler( propInfo ) );
                    }
                }
                return ContextGets[ propName ]( context );

            }
            else if (Exp.Nodes.Count==5) {

                // attribute mentioned: 
                // 0    1 2    3 4
                // prop @ attr . prop
                var propName = Exp.Nodes[ 0 ].Token.Text;
                var attrName = Exp.Nodes[ 2 ].Token.Text;
                var attrProp = Exp.Nodes[ 4 ].Token.Text;

                // lots of reflection, but attributes values do not change at runtime, so it will happen only once... I guess :^D
                var propInfo = context.GetType( ).GetProperty( propName );
                if ( propInfo != null ) {
                    var attributes = propInfo.GetCustomAttributes( true ).Where( o => ( o.GetType( ).Name == attrName ) || ( o.GetType( ).Name == attrName + "Attribute" ) ).ToArray( );
                    if ( attributes.Length == 1 ) {
                        var myAttr = attributes[ 0 ];
                        var attrPropInfo = myAttr.GetType( ).GetProperty( attrProp );
                        if ( attrPropInfo != null ) {
                            return attrPropInfo.GetValue( myAttr, null );
                        }
                    }
                }

            }

            return null;

        }

        #endregion

        #region event binding

        string EventName;
        Delegate MyEvent;
        ICommand cmd;

        void BindEvent( string EventName, ParseNode Exp ) {

            var propName = GetSimplePropertyName( Exp );
            
            if ( propName != null ) {

                var ctxProp = context.GetType( ).GetProperty( propName );
                if ( ctxProp != null ) {

                    var handler = Emmiter.CreateGetHandler( ctxProp );
                    cmd = handler( context ) as ICommand;
                    cmd.CanExecuteChanged += new EventHandler<EventArgs>( cmd_CanExecuteChanged );
                    if ( cmd != null ) {

                        AttachToEvent( EventName, ( ) => { 
                            if ( cmd.CanExecute( control ) ) cmd.Execute( control ); 
                        } );

                    }

                }

            }

        }

        void cmd_CanExecuteChanged( object sender, EventArgs e ) {
            control.Enabled = ( ( ICommand )sender ).CanExecute( control );
        }

        void AttachToEvent( string EventName, Action eventCall ) {
            if ( MyEvent != null ) return;

            this.EventName = EventName;
            var ev = control.GetType( ).GetEvent( EventName );
            if ( ev != null ) {

                var evType = ev.EventHandlerType;
                var eventParams = evType.GetMethod( "Invoke" ).GetParameters( );
                var parameters = eventParams.Select( p => Expression.Parameter( p.ParameterType, "x" ) );
                var body = Expression.Call( Expression.Constant( eventCall ), eventCall.GetType( ).GetMethod( "Invoke" ) );
                var lambda = Expression.Lambda( body, parameters.ToArray( ) );

                var del = Delegate.CreateDelegate( evType, lambda.Compile( ), "Invoke", false );
                MyEvent = del;

                try {
                    ev.AddEventHandler( control, del );
                }
                catch {
                    ;
                }
            };
        }

        #endregion

        #region collection binding

        void BindCollection( PropertyInfo Property, ParseNode Way, ParseNode Exp ) {

        }
        
        #endregion

        public void Unbind( ) {

            // unbind event
            if ( MyEvent != null ) {
                var ev = control.GetType( ).GetEvent( EventName );
                if ( ev != null ) {
                    ev.RemoveEventHandler( control, MyEvent );
                }
                cmd.CanExecuteChanged -= new EventHandler<EventArgs>( cmd_CanExecuteChanged );
            }

            context.PropertyChanged -= new PropertyChangedEventHandler( context_PropertyChanged );

        }

        string GetSimplePropertyName( ParseNode Exp ) {

            if ( ( Exp.Token.Type == TokenType.AddExpr ) && ( Exp.Nodes.Count == 1 ) ) {
                var inner = Exp.Nodes[ 0 ];
                if ( ( inner.Token.Type == TokenType.MultExpr ) && ( inner.Nodes.Count == 1 ) ) {
                    var atom = inner.Nodes[ 0 ];
                    if ( ( atom.Token.Type == TokenType.Atom ) && ( atom.Nodes.Count == 1 ) ) {
                        var dotted = atom.Nodes[ 0 ];
                        return GetSimpleDottedName( dotted );
                    }
                }
            }

            return null;
        }

        string GetSimpleDottedName( ParseNode Exp ) {
            if ( Exp.Token.Type == TokenType.DottedIdent ) {
                if ( Exp.Nodes.Count == 1 ) {
                    return Exp.Nodes[ 0 ].Token.Text;
                }
            }
            return null;
        }

    }
}
